package com.gallop.common.lock;

import com.gallop.common.CacheKeyAssembleHandler;
import com.gallop.common.lock.exception.AspectWrappedException;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.InvocationHandler;

import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@SuppressWarnings("unused")
@Slf4j
public class DistributeLocker {

    private static final String CACHE_KEY_PREFIX = "$$dis_lock:";
    private static final DistributedConstants.OnError DEFAULT_ERROR_HANDLER = ex ->
            log.error("", ex);
    private static final DistributedConstants.OnSuccess DEFAULT_ON_SUCCESS_HANDLER = () -> {
    };
    private static final DistributedConstants.OnFail DEFAULT_ON_FAIL_HANDLER = key ->
            log.error("try acquire distributed lock {} failed.", key);

    DistributeLocker() {
    }

    private CacheKeyAssembleHandler assembleHandler;
    private RedissonClient redissonClient;
    private static DistributeLocker INSTANCE;
    private static DistributeLocker INSTANCE_PROXY;
//    private static ApplicationContext applicationContext;

    public void lock(String key,
                     TimeUnit maxWaitTimeUnit,
                     long maxWait,
                     TimeUnit expireTimeUnit,
                     long expireAfter,
                     boolean fastFail,
                     DistributedConstants.OnSuccess onSuccess,
                     DistributedConstants.OnFail onFail,
                     DistributedConstants.OnError onError,
                     Set<Class<? extends Exception>> excludeOnThrows) throws Exception {
        RLock lock = redissonClient.getLock(CACHE_KEY_PREFIX + key);
        if (fastFail && lock.isLocked() && !lock.isHeldByCurrentThread()) {
            handleOnFail(onFail, key);
            return;
        }
        boolean lockAcquired = false;
        try {
            long expireMillis = expireTimeUnit.toMillis(expireAfter);
            if (maxWait <= 0) {
                lockAcquired = true;
                lock.lock(expireMillis, TimeUnit.MILLISECONDS);
            } else {
                long maxWaitMillis = maxWaitTimeUnit.toMillis(maxWait);
                lockAcquired = lock.tryLock(maxWaitMillis, expireMillis, TimeUnit.MILLISECONDS);
                if (!lockAcquired) {
                    handleOnFail(onFail, key);
                    return;
                }
            }
            onSuccess.execute();
        } catch (InterruptedException ex) {
            handleOnError(onError, ex);
        } catch (Exception ex) {
            Throwable actualException = (ex instanceof AspectWrappedException) ?
                    ex.getCause() : ex;
            if (excludeOnThrows == null) {
                handleOnError(onError, actualException);
                return;
            }
            if (excludeOnThrows.contains(actualException.getClass())
                    || excludeOnThrows.stream().anyMatch(e -> e.isAssignableFrom(actualException.getClass()))) {
                throw (Exception) actualException;
            }
            handleOnError(onError, actualException);
        } finally {
            if (lockAcquired && lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private void handleOnError(DistributedConstants.OnError onError, Throwable ex) {
        if (onError != null) {
            onError.handle(ex);
        } else {
            log.error("DistributeLocker missing OnError handler.", ex);
        }
    }

    private void handleOnFail(DistributedConstants.OnFail onFail, String key) {
        if (onFail != null) {
            onFail.handle(key);
        } else {
            log.warn("DistributeLocker missing OnFail handler. key={}", key);
        }
    }

    public void lock(DistributedLockOption option) throws Exception {
        lock(option.getKey(),
                option.getMaxWaitTimeUnit(),
                option.getMaxWait(),
                option.getExpireTimeUnit(),
                option.getExpireAfter(),
                option.isFastFail(),
                option.getOnSuccess() == null ? DEFAULT_ON_SUCCESS_HANDLER : option.getOnSuccess(),
                option.getOnFail() == null ? DEFAULT_ON_FAIL_HANDLER : option.getOnFail(),
                option.getOnError() == null ? DEFAULT_ERROR_HANDLER : option.getOnError(),
                option.getExceptionThrows());
    }


//    @Override
//    public void setApplicationContext(ApplicationContext context) throws BeansException {
////        applicationContext = context;
////        redissonClient = context.getBean(RedissonClient.class);
////        assembleHandler = context.getBean(CacheKeyAssembleHandler.class);
//    }

    public static DistributeLocker init(RedissonClient redissonClient, CacheKeyAssembleHandler assembleHandler) {
        INSTANCE = new DistributeLocker();
        INSTANCE.assembleHandler = assembleHandler;
        INSTANCE.redissonClient = redissonClient;
        return INSTANCE;
    }

    public static DistributeLocker getInstance() {
        if (INSTANCE_PROXY == null) {
            synchronized (DistributeLocker.class) {
                if (INSTANCE_PROXY != null) {
                    return INSTANCE_PROXY;
                }
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(DistributeLocker.class);
                enhancer.setCallback((InvocationHandler) (o, method, objects) -> {
                    if (method.getName().equals("getInstance")) {
                        return INSTANCE_PROXY;
                    }
                    if (INSTANCE == null) {
                        log.warn("DistributeLocker not initialized.");
                        return null;
                    }
//                    log.debug("proxy {}", method.getName());
                    return method.invoke(INSTANCE, objects);
                });
                INSTANCE_PROXY = (DistributeLocker) enhancer.create();
                // copy field refs
                INSTANCE_PROXY.assembleHandler = INSTANCE.assembleHandler;
                INSTANCE_PROXY.redissonClient = INSTANCE.redissonClient;
            }
        }
        return INSTANCE_PROXY;
    }

    @Data
    public static class DistributedLockOption {
        private String key;
        private TimeUnit maxWaitTimeUnit;
        private Long maxWait;
        private TimeUnit expireTimeUnit;
        private Long expireAfter;
        private boolean fastFail;

        private DistributedConstants.OnSuccess onSuccess;
        private DistributedConstants.OnError onError;
        private DistributedConstants.OnFail onFail;
        private Set<Class<? extends Exception>> exceptionThrows;

        public static DistributedLockOptionBuilder builder(String key) {
            return DistributedLockOptionBuilder.fromKey(key);
        }

        public static DistributedLockOptionBuilder builder(DistributedConstants.LockPrimaryKey primaryKey, List<Object> params) {
            return DistributedLockOptionBuilder.fromAssemble(primaryKey, params);
        }

        public static DistributedLockOptionBuilder builder(String primaryKey, List<Object> params) {
            return DistributedLockOptionBuilder.fromAssemble(DistributedConstants.LockPrimaryKey.wrap(primaryKey), params);
        }
    }

    @Getter
    @Setter
    @Accessors(chain = true)
    public static class DistributedLockOptionBuilder {

        private final String key;

        private TimeUnit maxWaitTimeUint;
        private Long maxWait;

        private TimeUnit expireTimeUnit;
        private Long expireAfter;
        private boolean fastFail;

        private DistributedConstants.OnSuccess onSuccess;
        private DistributedConstants.OnError onError;
        private DistributedConstants.OnFail onFail;
        private Set<Class<? extends Exception>> exceptionThrows;

        private DistributedLockOptionBuilder(String key) {
            this.key = key;
        }

        public static DistributedLockOptionBuilder fromKey(String key) {
            return new DistributedLockOptionBuilder(key);
        }

        public static DistributedLockOptionBuilder fromAssemble(DistributedConstants.LockPrimaryKey primaryKey, List<Object> params) {
            return new DistributedLockOptionBuilder(primaryKey.toPrimaryKeyString() + DistributeLocker.getInstance().assembleHandler.assemble(params));
        }

        public DistributedLockOption build() {
            DistributedLockOption ret = new DistributedLockOption();
            ret.setKey(this.key);
            ret.setMaxWait(this.maxWait);
            ret.setMaxWaitTimeUnit(this.expireTimeUnit);
            ret.setExpireAfter(this.expireAfter);
            ret.setExpireTimeUnit(this.expireTimeUnit);
            ret.setFastFail(this.fastFail);
            ret.setExceptionThrows(this.exceptionThrows);
            ret.setOnError(this.onError);
            ret.setOnFail(this.onFail);
            ret.setOnSuccess(this.onSuccess);
            return ret;
        }
    }
}
